﻿using System;
using Microsoft.DirectX.DirectSound;
using System.IO;
using System.Threading;

namespace SoundCapture
{
    public class DirectSoundCapture : IDisposable
    {
        private readonly int _numBuffers = 4;
        private readonly Capture _capture;
        private readonly CaptureBuffer _captureBuffer;
        private bool _started;
        private ThreadStart _threadProc;
        private Thread _captureThread;

        public DirectSoundCapture(int bufferSize, int samplesPerSecond, short bitsPerSample, short nChannels, Action<byte[]> callback, bool start)
        {
            WaveFormat format = MakeWaveFormat(samplesPerSecond, bitsPerSample, nChannels);
            int captureBufferSize = _numBuffers * bufferSize;

            _capture = new Capture();

            CaptureBufferDescription cbd = new CaptureBufferDescription()
            {
                Format = format,
                BufferBytes = captureBufferSize
            };

            _captureBuffer = new CaptureBuffer(cbd, _capture);

            AutoResetEvent resetEvent = SetNotificationPoints(bufferSize);

            _threadProc = delegate
            {
                _captureBuffer.Start(true);
                int offset = 0;
                while (_started)
                {
                    int rp;
                    int cp;
                    _captureBuffer.GetCurrentPosition(out cp, out rp);

                    while (Distance(offset, rp, captureBufferSize) < bufferSize || X_inside_YZ(offset, rp, cp))
                    {
                        resetEvent.WaitOne();
                        if (!_started)
                            break;
                        _captureBuffer.GetCurrentPosition(out cp, out rp);
                    }

                    using (MemoryStream stream = new MemoryStream())
                    {
                        _captureBuffer.Read(offset, stream, bufferSize, LockFlag.None);
                        if (_started)
                            callback(stream.ToArray());
                    }
                    offset = (offset + bufferSize) % captureBufferSize;
                }
                _captureBuffer.Stop();
            };

            if (start)
                Start();
        }

        public bool IsStarted
        {
            get { return _started; }
        }

        public void Start()
        {
            if (!_started)
            {
                _captureThread = new Thread(_threadProc);
                _started = true;
                _captureThread.Start();
            }
        }

        public void Stop()
        {
            _started = false;
            if (_captureThread != null)
            {
                if (!_captureThread.Join(1000))
                {
                    _captureThread.Abort();
                }
                _captureThread = null;
            }
        }

        private AutoResetEvent SetNotificationPoints(int notifySize)
        {
            AutoResetEvent resetEvent = new AutoResetEvent(false);
            Notify notify = new Notify(_captureBuffer);

            BufferPositionNotify[] nn = new BufferPositionNotify[_numBuffers];
            for (int i = 0; i < _numBuffers; i++)
            {
                var bpn = new BufferPositionNotify();
                bpn.Offset = (i + 1) * notifySize - 1;
                bpn.EventNotifyHandle = resetEvent.SafeWaitHandle.DangerousGetHandle();
                nn[i] = bpn;
            }

            notify.SetNotificationPositions(nn);
            return resetEvent;
        }

        private static WaveFormat MakeWaveFormat(int samplesPerSecond, short bitsPerSample, short nChannels)
        {
            WaveFormat format = new WaveFormat();
            format.BitsPerSample = bitsPerSample;
            format.Channels = nChannels;
            format.SamplesPerSecond = samplesPerSecond;
            format.FormatTag = WaveFormatTag.Pcm;

            format.BlockAlign = (short)(format.Channels * (format.BitsPerSample / 8));
            format.AverageBytesPerSecond = format.SamplesPerSecond * format.BlockAlign;
            return format;
        }

        private static bool X_inside_YZ(int x, int y, int z)
        {
            return Math.Sign(x - y) * Math.Sign(x - z) * Math.Sign(z - y) < 0;
        }

        private static int Distance(int x, int y, int size)
        {
            if (y >= x)
                return y - x;
            else
                return size + y - x;
        }

        public void Dispose()
        {
            try
            {
                Stop();
            }
            catch { }
            _captureBuffer.Dispose();
            _capture.Dispose();
        }
    }
}
